/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.maven;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.PumpStreamHandler;
import org.apache.tools.ant.types.Commandline;

import ws.quokka.core.bootstrap_util.IOUtils;
import ws.quokka.core.bootstrap_util.Logger;
import ws.quokka.core.repo_resolver.ResolvedPath;
import ws.quokka.core.repo_resolver.Resolver;
import ws.quokka.core.repo_spi.RepoArtifact;
import ws.quokka.core.repo_spi.RepoArtifactId;
import ws.quokka.core.repo_spi.RepoConflict;
import ws.quokka.core.repo_spi.RepoDependency;
import ws.quokka.core.repo_spi.RepoPath;
import ws.quokka.core.repo_spi.RepoPathSpec;
import ws.quokka.core.repo_spi.RepoXmlConverter;
import ws.quokka.core.repo_spi.Repository;
import ws.quokka.core.repo_spi.UnresolvedArtifactException;
import ws.quokka.core.util.Strings;
import ws.quokka.core.util.xml.Document;
import ws.quokka.core.util.xml.Element;
import ws.quokka.core.version.Version;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;


public class MavenImporter {
    //~ Static fields/initializers -------------------------------------------------------------------------------------

    private static final String DEFAULT_TEMPLATE = "<?xml version=\"1.0\"?>\n"
        + "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n" //
        + "    <modelVersion>4.0.0</modelVersion>\n" //
        + "    <groupId>dummy</groupId>\n" //
        + "    <artifactId>dummy</artifactId>\n" //
        + "    <packaging>pom</packaging>\n" //
        + "    <version>dummy</version>\n" //
        + "    <build>\n" //
        + "        <plugins>\n" //
        + "            <plugin>\n" //
        + "                <groupId>ws.quokka.maven</groupId>\n" //
        + "                <artifactId>repo-plugin</artifactId>\n" //
        + "                <version>@pluginVersion@</version>\n" //
        + "            </plugin>\n" //
        + "        </plugins>\n" //
        + "    </build>\n" //
        + "    <pluginRepositories>\n" //
        + "        <pluginRepository>\n" //
        + "            <id>quokka.ws</id>\n" //
        + "            <name>Quokka Maven Plugin Repository</name>\n" //
        + "            <url>http://quokka.ws/repository/maven</url>\n" //
        + "            <snapshots>\n" //
        + "                <enabled>true</enabled>\n" //
        + "            </snapshots>\n" //
        + "            <releases>\n" //
        + "                <enabled>true</enabled>\n" //
        + "            </releases>\n" //
        + "        </pluginRepository>\n" //
        + "    </pluginRepositories>" + "    <dependencies>\n" //
        + "        <dependency/>\n" //
        + "    </dependencies>\n" //
        + "</project>";

    //~ Instance fields ------------------------------------------------------------------------------------------------

    private String template;
    private String mvnCommand;
    private Logger log;
    private IdMapper mapper;
    private IOUtils ioUtils = new IOUtils();
    private String pluginVersion;
    private Repository repository;
    private boolean force;
    private Map idVersions = new HashMap();

    //~ Constructors ---------------------------------------------------------------------------------------------------

    public MavenImporter(String template, String mvnCommand, Logger log, String pluginVersion, IdMapper mapper,
        Repository repository, boolean force, Collection existingIds) {
        this.template = (template != null) ? template : DEFAULT_TEMPLATE;
        this.mvnCommand = (mvnCommand != null) ? mvnCommand : "mvn";
        this.log = log;
        this.pluginVersion = (pluginVersion != null) ? pluginVersion : "1.0";
        this.mapper = mapper;
        this.repository = repository;
        this.force = force;

        // Create an index based on the unversioned id
        for (Iterator i = existingIds.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            RepoArtifactId unversioned = id.toUnversionedId();
            Set versions = (Set)idVersions.get(unversioned);

            if (versions == null) {
                versions = new TreeSet();
                idVersions.put(unversioned, versions);
            }

            versions.add(id.getVersion());
        }
    }

    //~ Methods --------------------------------------------------------------------------------------------------------

    public ResolvedMavenArtifact resolveMavenArtifact(MavenId mavenId) {
        File project = createTemporaryProject(template, mavenId);
        File output = createTempFile("maventree", ".txt");

        try {
            boolean resolved = executeMaven(mavenId, mvnCommand, project, output,
                    new String[] { "[INFO] Failed to resolve artifact." });

            if (!resolved) {
                throw new BuildException("Maven could not resolved the artifact: " + mavenId);
            }

//            System.out.println(ioUtils.fileToString(output));
            return parse(output);
        } finally {
            if (!log.isDebugEnabled()) {
                output.delete();
            }
        }
    }

    protected ResolvedMavenArtifact parse(File file) {
        ResolvedMavenArtifact artifact = new ResolvedMavenArtifact();
        Document document = Document.parse(file);
        Element rootEl = document.getRoot();

        Element dependenciesEl = rootEl.getChild("dependencies");

        if (dependenciesEl != null) {
            for (Iterator i = dependenciesEl.getChildren("dependency").iterator(); i.hasNext();) {
                Element dependencyEl = (Element)i.next();
                artifact.getDependencies().add(parseDependency(dependencyEl));
            }
        }

        Element licensesEl = rootEl.getChild("licenses");

        if (licensesEl != null) {
            for (Iterator i = licensesEl.getChildren("license").iterator(); i.hasNext();) {
                Element licenseEl = (Element)i.next();
                String name = licenseEl.getAttribute("name");
                LicenseMapping mapping = mapper.getLicenseMapping(name);
                mapping = (mapping != null) ? mapping : new LicenseMapping(name, null);
                artifact.getLicenses().add(mapping);
            }
        }

        Element descriptionEl = rootEl.getChild("description");

        if (descriptionEl != null) {
            artifact.setDescription(descriptionEl.getText());
        }

        Element pathEl = rootEl.getChild("path");

        if (pathEl != null) {
            artifact.setPath(parseDependency(pathEl.getChild("dependency")));
        }

        return artifact;
    }

    private MavenDependency parseDependency(Element el) {
        MavenDependency dependency = new MavenDependency();
        MavenId mavenId = new MavenId(el.getAttribute("groupId"), el.getAttribute("artifactId"),
                el.getAttribute("type"), el.getAttribute("classifier"), el.getAttribute("version"));
        dependency.setMavenId(mavenId);

        String file = el.getAttribute("file");
        dependency.setFile((file == null) ? null : new File(file));

        String optional = el.getAttribute("optional");
        dependency.setOptional((optional == null) ? null : Boolean.valueOf(optional));
        dependency.setState(el.getAttribute("state"));

        dependency.setId(mapper.toId(mavenId));

        for (Iterator i = el.getChildren("dependency").iterator(); i.hasNext();) {
            Element dependencyEl = (Element)i.next();
            dependency.addDependency(parseDependency(dependencyEl));
        }

        return dependency;
    }

    public File createTempFile(String prefix, String suffix) {
        try {
            return File.createTempFile(prefix, suffix);
        } catch (IOException e) {
            throw new BuildException(e);
        }
    }

    /**
     * Executes mvn with the dependency:tree goal on the project file given, writing the results to output
     *
     * @param unresolvedMessages Is a set of strings to indentify a unresolved artifact versus a general failure
     * @return true if the artifact is resolved
     */
    protected boolean executeMaven(MavenId id, String mvnCommand, File project, File output, String[] unresolvedMessages) {
        Execute execute = new Execute();
        execute.setVMLauncher(false); // mvn is a batch file and therefore must be launched via the shell

        ByteArrayOutputStream out = new ByteArrayOutputStream(1000);
        ByteArrayOutputStream err = new ByteArrayOutputStream(1000);
        execute.setStreamHandler(new PumpStreamHandler(out, err, System.in));
        execute.setCommandline(createCommand(mvnCommand, project, output, id));

        try {
            execute.execute();
        } catch (IOException e) {
            throw new BuildException("Unable to execute mvn", e);
        }

        if (log.isDebugEnabled()) {
            log.debug("Maven out: " + out.toString());
            log.debug("Maven err: " + err.toString());
        }

//        System.out.println(out);
//        System.out.println(err);
        // Exit value doesn't appear to be set and/or propogated (at least on os x)
        // Therefore, test the output file for content
        String[] lines = Strings.split(out.toString(), "\n");

        if (!output.exists() || (output.length() == 0)) {
            // Is this a fatal exception or an unresolved artifact?
            // Maven only seems to write to stdout ...
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i];

                for (int j = 0; j < unresolvedMessages.length; j++) {
                    String message = unresolvedMessages[j];

                    if (line.indexOf(message) != -1) {
                        // Contains info about why it could be resolved
                        log.info(out.toString());
                        log.info(err.toString());

                        return false;
                    }
                }
            }

            throw new BuildException("Unexpected error occurred while trying to execute maven\nOutput:\n"
                + out.toString() + "Error:\n" + err.toString());
        } else {
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i];

                if (line.startsWith("[WARNING]")) {
                    log.warn(line);
                }
            }
        }

        return true;
    }

    protected String[] createCommand(String mvnCommand, File project, File out, MavenId id) {
        Commandline command = new Commandline();
        command.setExecutable(mvnCommand);
        command.createArgument().setValue("-f");
        command.createArgument().setValue(project.getPath());
        command.createArgument().setValue("ws.quokka.maven:repo-plugin:quokkatree");
        command.createArgument().setValue("-DoutputFile=" + out.getPath());
        command.createArgument().setValue("-Dartifact=" + id.toString());

        if (log.isDebugEnabled()) {
            log.debug(command.describeCommand());
        }

        return command.getCommandline();
    }

    protected File createTemporaryProject(String template, MavenId id) {
        String dependency = "<dependency>\n" + tag("groupId", id.getGroupId()) + tag("artifactId", id.getArtifactId())
            + tag("version", id.getVersion()) + tag("type", id.getType()) + tag("scope", "runtime")
            + ((id.getClassifier() == null) ? "" : tag("classifier", id.getClassifier())) + "</dependency>";
        String project = Strings.replace(template, "<dependency/>", dependency);
        project = Strings.replace(project, "@pluginVersion@", pluginVersion);

        File temp = createTempFile("dummyproject", ".xml");
        temp.deleteOnExit();

        ioUtils.stringToFile(project, temp);

        return temp;
    }

    private String tag(String name, String value) {
        return "<" + name + ">" + value + "</" + name + ">\n";
    }

    public void importArtifact(MavenId id) {
        importArtifact(id, new HashSet());
    }

    private boolean exists(MavenId id) {
        RepoArtifactId artifactId = mapper.toId(id);

        if (artifactId == null) {
            return false;
        }

        try {
            repository.resolve(artifactId, false);

            return true;
        } catch (UnresolvedArtifactException e) {
            return false;
        }
    }

    private void importArtifact(MavenId id, Set imported) {
        // Check up front as resolving via maven is slow
        if (exists(id)) {
            log.info("Skipping " + id.toString() + " as it is already imported");

            return;
        }

        log.info("<--- Importing artifact: " + id.toString() + " --->");

        ResolvedMavenArtifact mavenArtifact = resolveMavenArtifact(id);

        if (mavenArtifact.containsUnmappedIds()) {
            log.info(mavenArtifact.toString());
            throw new UnmappedArtifactException();
        }

        for (Iterator i = mavenArtifact.getLicenses().iterator(); i.hasNext();) {
            LicenseMapping mapping = (LicenseMapping)i.next();

            if (mapping.getId() != null) {
                repository.resolve(mapping.getId(), false); // Check that the license exists
            }
        }

//        log.info("Checking the quokka repository for existing dependencies...");
        List prereqs = mavenArtifact.getPrerequisites(repository);
        log.info(mavenArtifact.toString());

        if (prereqs.size() != 0) {
            for (Iterator i = prereqs.iterator(); i.hasNext();) {
                MavenId prereq = (MavenId)i.next();

                if (!imported.contains(prereq) && !prereq.equals(id)) {
                    importArtifact(prereq, imported);
                }
            }
        }

        // Check that the artifact is not already in the repository
        if (!mavenArtifact.getPath().isRequiresImport()) {
            log.info("Skipping " + id.toString() + " as it is already imported");

            return;
        }

        // At this point, actually import the mavenArtifact
        log.info("Importing " + id);

        RepoArtifact artifact = new RepoArtifact(mapper.toId(id));
        IdMapping idMapping = mapper.getIdMapping(id);
        artifact.setImportedFrom("maven2=" + id + "; mapping=" + idMapping.getMavenId() + "#"
            + idMapping.getId().toShortString());
        artifact.setLocalCopy(mavenArtifact.getPath().getFile());

        // Get the previous artifact if it exists
        RepoArtifact previous = null;
        Set versions = (Set)idVersions.get(artifact.getId().toUnversionedId());

        if (versions != null) {
            Version previousVersion = (Version)new ArrayList(versions).get(versions.size() - 1);
            previous = repository.resolve(new RepoArtifactId(artifact.getId().getGroup(), artifact.getId().getName(),
                        artifact.getId().getType(), previousVersion), false);
        }

        // Use the previous description, license and conflicts if possible
        if (previous != null) {
            artifact.setDescription(previous.getDescription());

            for (Iterator i = previous.getLicenses().iterator(); i.hasNext();) {
                RepoArtifactId licenseId = (RepoArtifactId)i.next();
                artifact.addLicense(licenseId);
            }

            for (Iterator i = previous.getConflicts().iterator(); i.hasNext();) {
                RepoConflict conflict = (RepoConflict)i.next();
                artifact.addConflict(new RepoConflict(conflict.getId(), conflict.getKind()));
            }

            // TODO: Should paths and overrides be copied too?
        } else {
            if (mavenArtifact.getDescription() != null) {
                String description = mavenArtifact.getDescription().trim();

                if (!description.equals("")) {
                    artifact.setDescription(mavenArtifact.getDescription());
                }
            }

            for (Iterator i = mavenArtifact.getLicenses().iterator(); i.hasNext();) {
                LicenseMapping mapping = (LicenseMapping)i.next();

                if (mapping.getId() != null) {
                    artifact.addLicense(mapping.getId());
                }
            }

            if (((mavenArtifact.getLicenses() == null) && artifact.getId().getGroup().equals("apache"))
                    || artifact.getId().getGroup().startsWith("apache.")) {
                log.info("Adding default Apache License 2.0 to " + artifact.getId().toShortString());
                artifact.addLicense(new RepoArtifactId("license.apache", "apache", "license", "2.0"));
            }
        }

        artifact.addPath(new RepoPath("runtime", "Runtime path"));

        // Add the optional dependencies (these aren't in the transitive runtime path)
        for (Iterator i = mavenArtifact.getDependencies().iterator(); i.hasNext();) {
            MavenDependency mavenDependency = (MavenDependency)i.next();
            RepoDependency dependency = new RepoDependency();
            dependency.setId(mavenDependency.getId());

            if (mavenDependency.getOptional().booleanValue()) {
                dependency.addPathSpec(new RepoPathSpec("runtime", "runtime", null, Boolean.TRUE, Boolean.FALSE));
                artifact.addDependency(dependency);
            }
        }

        // Add the runtime path, taking care to add exclusions and overrides where necessary
        for (Iterator i = mavenArtifact.getPath().getDependencies().iterator(); i.hasNext();) {
            MavenDependency mavenDependency = (MavenDependency)i.next();
            RepoDependency dependency = new RepoDependency();
            dependency.setId(mavenDependency.getId());
            dependency.addPathSpec(new RepoPathSpec("runtime", "runtime", getOptions(mavenDependency), Boolean.TRUE,
                    Boolean.TRUE));
            artifact.addDependency(dependency);
        }

        StringWriter writer = new StringWriter();
        RepoXmlConverter.toXml(artifact, writer);
        System.out.println(writer.toString());

        sanityCheck(mavenArtifact, artifact);

        repository.install(artifact);
        imported.add(id);
    }

    private void sanityCheck(ResolvedMavenArtifact mavenArtifact, RepoArtifact artifact) {
        // Get the quokka path
        Resolver resolver = new Resolver(repository, log);
        ResolvedPath path = resolver.resolvePath("runtime", artifact, new ArrayList(), true, false);

        String message = compare(mavenArtifact, artifact, path);

        if (message != null) {
            log.warn(message);

            if (!force) {
                throw new BuildException("The quokka and maven paths do not match for artifact: " + artifact.getId()
                    + "\nYou can force the artifact to be added regardless using -Dforce=true");
            }
        }
    }

    /**
     * Compares the maven artifact path with quokka path given. Returns null if they match,
     * or an error message if they don't
     */
    public String compare(ResolvedMavenArtifact mavenArtifact, RepoArtifact artifact, ResolvedPath path) {
        // Get the maven path in terms of quokka ids
        Set mavenDeps = new HashSet();
        mavenArtifact.getAllDependencies(mavenDeps, mavenArtifact.getPath());

        for (Iterator i = mavenDeps.iterator(); i.hasNext();) {
            MavenDependency dependency = (MavenDependency)i.next();

            if (!dependency.getState().equals("included")) {
                i.remove();
            }
        }

        Set mavenPath = new TreeSet();

        for (Iterator i = mavenDeps.iterator(); i.hasNext();) {
            MavenDependency dependency = (MavenDependency)i.next();
            mavenPath.add(dependency.getId());
        }

        // Get the quokka path
        Set quokkaPath = new TreeSet();
        quokkaPath.add(artifact.getId());

        for (Iterator i = path.getArtifacts().iterator(); i.hasNext();) {
            RepoArtifact repoArtifact = (RepoArtifact)i.next();
            quokkaPath.add(repoArtifact.getId());
        }

        if (mavenPath.equals(quokkaPath)) {
            return null;
        }

        return "Paths do not match for " + artifact.getId().toShortString() + "\n" + "    quokka: "
        + toString(quokkaPath) + "\n     maven: " + toString(mavenPath);
    }

    private String toString(Set quokkaPath) {
        StringBuffer sb = new StringBuffer();

        for (Iterator i = quokkaPath.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            sb.append(id.toShortString());

            if (i.hasNext()) {
                sb.append(", ");
            }
        }

        return sb.toString();
    }

    private String getOptions(MavenDependency mavenDependency) {
        // TODO ... automate options to take care of conflicts
        // At the moment, -Dforce=true must be used and the result must be manually edited
        return null;
    }
}
